using System;
using System.Collections.Generic;
using System.Text;
using Decav.Gatsb.Application;
using Decav.Gatsb.Data;
using Decav.Security;
using System.Net.Mail;
using Decav.Gatsb.Configuration;
using System.Web;
using System.Web.Security;
using System.IO;
using System.Web.Caching;
using Decav.Reality;

namespace Decav.Gatsb.Web
{
    /// <summary>
    /// The web application version of the <see cref="GatsbApplication"/>.
    /// </summary>
    [Serializable]
    public class GatsbWebApplication : GatsbApplication
    {
        /// <summary>
        /// Gets the current <see cref="GatsbWebApplication"/> instance.
        /// </summary>
        public static new GatsbWebApplication Current
        {
            get
            {
                return (GatsbWebApplication)GatsbApplication.Current;
            }
        }

        /// <summary>
        /// Sets a status message and then redirects to the home page, displaying the status.
        /// </summary>
        /// <param name="status">The status to configure the homepage with.</param>
        public void SetStatusAndRedirect(string status)
        {
            if (status == null)
                throw new ArgumentNullException("status");

            HttpContext.Current.Session["Homepage_Status"] = status;
            HttpContext.Current.Response.Redirect("~/Home.aspx", true);
        }

        protected override void OnBeforeGetCurrentUser(EventArgs e)
        {
            if (HttpContext.Current.User.Identity.IsAuthenticated && !UserPresent)
            {
                using (IGatsbDataAdapter adapt = GetDataAdapter())
                    CurrentUser = adapt.GetUser(new Guid(HttpContext.Current.User.Identity.Name));
            }
        }

        /// <summary>
        /// Logs out the <see cref="CurrentUser"/>.
        /// </summary>
        public void SignOut()
        {
            CurrentUser = null;
            FormsAuthentication.SignOut();
            HttpContext.Current.Response.Redirect("~/Default.aspx", true);
        }

        /// <summary>
        /// Creates the URL to the search page based on a criteria set.
        /// </summary>
        /// <param name="criteria">The criteria to search on.</param>
        /// <param name="people">True when searching for people.</param>
        /// <param name="places">True when searching for places.</param>
        /// <returns>The URL to the search.</returns>
        public string CreateSearchUrl(string criteria, bool people, bool places)
        {
            if (criteria == null)
                throw new ArgumentNullException("criteria");

            StringBuilder builder = new StringBuilder();
            builder.Append("~/Search.aspx?q=");
            builder.Append(HttpContext.Current.Server.UrlEncode(criteria));

            builder.AppendFormat("&people={0}", people);
            builder.AppendFormat("&places={0}", places);

            return builder.ToString();
        }

        /// <summary>
        /// Sets the users current location to the specified location.
        /// </summary>
        /// <param name="locationId">The ID of the location to set the user as in.</param>
        public override void SetUserLocation(Guid location)
        {
            using (IGatsbDataAdapter adapt = GetDataAdapter())
                adapt.SetUserLocation(CurrentUser.Id, location);
        }

        /// <summary>
        /// Clears the location that the user specifies himself at.
        /// </summary>
        public override void ClearUserLocation()
        {
            using (IGatsbDataAdapter adapt = GetDataAdapter())
                adapt.SetUserLocation(CurrentUser.Id, Guid.Empty);
        }

        /// <summary>
        /// Gets the current location of the specified user.
        /// </summary>
        /// <param name="user">The user to get the location for.</param>
        public override LocationCheckin GetUserLocation(User user)
        {
            if (user == null)
                throw new ArgumentNullException("user");

            // Cache the current user current location checkin so we only get it once.
            // We often need to get this users current location more than once....
            if (UserPresent && user.Id == CurrentUser.Id && HttpContext.Current.Items["CurrentUser_CurrentLocationCheckin"] != null)
                return (LocationCheckin)HttpContext.Current.Items["CurrentUser_CurrentLocationCheckin"];

            using (IGatsbDataAdapter adapt = GetDataAdapter())
            {
                LocationCheckin checkin = adapt.GetUserCurrentLocation(user.Id);

                if (UserPresent && user.Id == CurrentUser.Id)
                    HttpContext.Current.Items["CurrentUser_CurrentLocationCheckin"] = checkin;

                return checkin;
            }
        }

        /// <summary>
        /// Gets a data adapter that provides access to the database.
        /// </summary>
        /// <returns>The data adapter to use with this application instance.</returns>
        public override IGatsbDataAdapter GetDataAdapter()
        {
            return new GatsbSqlDataAdapter();
        }

        /// <summary>
        /// Gets the <see cref="User"/> with the specified identifier.
        /// </summary>
        /// <param name="userId">The unique identifier of the user.</param>
        /// <returns>The user with the specified identifier.</returns>
        public override User GetUser(Guid userId)
        {
            lock(_usersCacheSyncRoot)
            {
                if (CurrentUser != null && userId == CurrentUser.Id)
                    return CurrentUser;

                // Hold a cache of users that were gotten on this request as
                // not to flood the database when asking for the same user over
                // and over.
                Dictionary<Guid, User> users = null;
                if (HttpContext.Current.Items.Contains(GetUserItemsCacheName))
                {
                    users = (Dictionary<Guid, User>)HttpContext.Current.Items[GetUserItemsCacheName];
                }
                else
                {
                    users = new Dictionary<Guid, User>();
                    HttpContext.Current.Items.Add(GetUserItemsCacheName, users);
                }

                if (users.ContainsKey(userId))
                    return users[userId];

                User usr = null;
                using (IGatsbDataAdapter adapt = GetDataAdapter())
                    usr = adapt.GetUser(userId);

                users.Add(userId, usr);
                return usr;
            }
        }

        private object _usersCacheSyncRoot = new object();
        private const string GetUserItemsCacheName = "RequestUsers";

        /// <summary>
        /// Gets the location that the unique identifier defines.
        /// </summary>
        /// <param name="locationId">The unique identifier of the location to get.</param>
        /// <returns>The location of the id.</returns>
        public override Location GetLocation(Guid locationId)
        {
            string cacheKey = string.Format("Gatsb.Location_{0}", locationId);
            Location loc = HttpContext.Current.Cache[cacheKey] as Location;

            if (loc != null)
                return loc;

            using (IGatsbDataAdapter adapt = GetDataAdapter())
                loc = adapt.GetLocation(locationId);

            HttpContext.Current.Cache.Add(cacheKey, loc, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.BelowNormal, null);
            return loc;
        }

        /// <summary>
        /// Creates a new user of the Gatsb application.
        /// </summary>
        /// <param name="user">The user to create.</param>
        /// <param name="password">The password of the user.</param>
        public override void CreateNewUser(User user, HashedPassword password)
        {
            if (user == null)
                throw new ArgumentNullException("user");

            using (IGatsbDataAdapter adapt = GetDataAdapter())
                adapt.CreateUser(user, password);

            CurrentUser = user;
            SendSmsValidation();
        }

        /// <summary>
        /// Attempts to validate a users validation code, such as one sent by the <see cref="SendSmsValidation"/>.
        /// </summary>
        /// <param name="code">The code to validate</param>
        /// <returns>True if validated, or if no code was needed, else false.</returns>
        public override bool TryValidateUserCode(UserValidationCode code)
        {
            if (!UserPresent)
                throw new AuthenticationException("The user is not currently authenticated and cannot be validated.");

            using (IGatsbDataAdapter adapt = GetDataAdapter())
            {
                return adapt.ValidateCode(CurrentUser.Id, code);
            }
        }

        /// <summary>
        /// Authenticates a username and password against the data source.
        /// </summary>
        /// <param name="email">The email address used as a username.</param>
        /// <param name="password">The users hashed password value.</param>
        /// <returns>The authentication result.</returns>
        public override UserLoginResult AuthenticateUser(EmailAddress email, HashedPassword password)
        {
            return AuthenticateUser(email, password, false);
        }

        /// <summary>
        /// Authenticates a username and password against the data source.
        /// </summary>
        /// <param name="email">The email address used as a username.</param>
        /// <param name="password">The users hashed password value.</param>
        /// <param name="redirect">True when the application should redirect
        /// from the login page using the "RedirectUrl" in the query string,
        /// or the default page.</param>
        /// <returns>The authentication result.</returns>
        public UserLoginResult AuthenticateUser(EmailAddress email, HashedPassword password, bool redirect)
        {
            UserLoginResult result = null;
            using (IGatsbDataAdapter adapt = GetDataAdapter())
                result = adapt.AuthenticateUser(email, password);

            if (result.Authenticated)
            {
                CurrentUser = result.User;
                if (redirect)
                {
                    if (result.RequiresValidation)
                        HttpContext.Current.Response.Redirect("~/ValidatePhone.aspx", true);

                    if (result.RequiresSetup)
                        HttpContext.Current.Response.Redirect("~/CompleteSetup.aspx", true);

                    FormsAuthentication.RedirectFromLoginPage(result.User.Id.ToString(), false);
                }
                else
                {
                    FormsAuthentication.SetAuthCookie(result.User.Id.ToString(), false);
                }
            }

            return result;
        }

        /// <summary>
        /// Sends the short-messaging-system text message to the <see cref="CurrentUser"/> with
        /// a validation code.
        /// </summary>
        public override void SendSmsValidation()
        {
            if (!UserPresent)
                throw new AuthenticationException("The user is not currently authenticated and cannot be sent an SMS.");

            UserValidationCode code = UserValidationCode.Generate();

            using (IGatsbDataAdapter adapt = GetDataAdapter())
                adapt.AddValidationCode(CurrentUser.Id, code);

            EmailAddress notificationAddress = CurrentUser.TelephoneServiceProvider.CreateSmsAddress(CurrentUser.MobilePhoneNumber);
            MailMessage message = new MailMessage();
            message.To.Add(new MailAddress(notificationAddress.ToString()));
            message.Body = string.Format("Thank you for registering at gatsb.com.\r\nYour validation code is '{0}'.", code);
            SendMail(message);
        }

        /// <summary>
        /// Indicates whether or not a <see cref="User"/> has a profile image.
        /// </summary>
        /// <param name="userId">The User ID to check for an image for.</param>
        /// <returns>True when the user has an image, else false.</returns>
        public override bool UserHasProfileImage(Guid userId)
        {
            // Check for the original file.  If it exists, than the user has an image.
            string filePath = Path.Combine(GatsbConfigurationSection.Current.UserUploadedImagesFolder, userId.ToString());
            return File.Exists(filePath);
        }

        /// <summary>
        /// Adds a user as a friend of the <see cref="CurrentUser"/>
        /// </summary>
        /// <param name="userId">The unique identifier of the user to add as a friend.</param>
        public override void AddFriend(Guid userId)
        {
            if (userId == CurrentUser.Id)
                throw new InvalidOperationException("Cannot befriend yourself.");

            User befriendedUser = null;
            using (IGatsbDataAdapter adapt = GetDataAdapter())
            {
                adapt.AddFriend(userId);
                befriendedUser = adapt.GetUser(userId);
            }
            
            string notificationBody = null;
            string notificationSubject = null;
            switch (befriendedUser.Friendship)
            {
                case FriendshipStatus.FriendshipRequested:
                    notificationBody = string.Format("{0} has added you as a friend on Gatsb.<br/><br/>To approve or deny this friendship, click the link below.<br/><a href=\"{1}\">{1}<a>.",
                        CurrentUser.Name, "http://www.gatsb.com/Inbox.aspx");
                    notificationSubject = string.Format("{0} has requested to add you as a friend", CurrentUser.Name);
                    break;

                case FriendshipStatus.MutualFriends:
                    notificationBody = string.Format("{0} has approved your friendship request.<br/><br/>To see {1}'s profile, click the link below.<br/><a href=\"http://www.gatsb.com/Person.aspx?id={2}\">http://www.gatsb.com/Person.aspx?id={2}</a>",
                        CurrentUser.Name, CurrentUser.Name.FirstName, CurrentUser.Id);
                    notificationSubject = string.Format("{0} has approved your friendship request", CurrentUser.Name);
                    break;
            }

            MailMessage message = new MailMessage();
            message.To.Add(new MailAddress(befriendedUser.Email.ToString(), befriendedUser.Name.ToString()));
            message.Subject = notificationSubject;
            message.Body = notificationBody;
            message.IsBodyHtml = true;

            if (notificationBody != null)
                SendMail(message);
        }

        /// <summary>
        /// Posts a <see cref="PostComment"/> to a post.
        /// </summary>
        /// <param name="comment">The comment to post.</param>
        public override void AddPostComment(PostComment comment)
        {
            if (comment == null)
                throw new ArgumentNullException("comment");

            Post post = null;
            using (IGatsbDataAdapter adapt = GetDataAdapter())
            {
                adapt.PostComment(comment);
                post = adapt.GetPost(comment.PostId);
            }

            if (CurrentUser.Id != post.Owner.Id)
            {
                MailMessage message = new MailMessage();
                message.To.Add(new MailAddress(post.Owner.Email.ToString(), post.Owner.Name.ToString()));
                message.Subject = string.Format("{0} posted a comment about a post of yours", CurrentUser.Name);
                message.Body = string.Format(
                    "{0} posted a comment about a post you made at {1} {2}.<br/><br/>Follow the link below to see the comment.<br/><a href=\"http://www.gatsb.com/post.aspx?id={3}#comments\">http://www.gatsb.com/post.aspx?id={3}#comments</a>",
                    post.Owner.Name,
                    post.Location.Name,
                    StringFormatting.ToTimeAgo(post.CreateDate),
                    post.Id);
                message.IsBodyHtml = true;

                SendMail(message);
            }
        }

        /// <summary>
        /// Rejects a friendship request from a user, or ends a current friendship.
        /// </summary>
        /// <param name="userId">The user to reject the friendship request of</param>
        public override void EndFriendship(Guid userId)
        {
            using (IGatsbDataAdapter adapt = GetDataAdapter())
                adapt.EndFriendship(userId);
        }

        /// <summary>
        /// Gets the status information for the <see cref="CurrentUser"/>.
        /// </summary>
        public override UserStatusInfo GetStatus()
        {
            using (IGatsbDataAdapter adapt = GetDataAdapter())
                return adapt.GetUserStatus();
        }

        /// <summary>
        /// Sends an SMS message to a user's telephone.
        /// </summary>
        /// <param name="recipientUserId">The recipient of the message.</param>
        /// <param name="senderAddress">The address to send to.</param>
        /// <param name="body">The message body to send.</param>
        public override void SendUserSms(User recipientUser, EmailAddress senderAddress, string body)
        {
            if (recipientUser == null)
                throw new ArgumentNullException("recipientUser");

            if (body == null)
                throw new ArgumentNullException("body");

            if (senderAddress == EmailAddress.Empty)
                senderAddress = new EmailAddress(GatsbConfigurationSection.Current.NotificationFromAddress);


            EmailAddress recipientAddress = (recipientUser.AlternateSmsAddress != EmailAddress.Empty) ? 
                recipientUser.AlternateSmsAddress : 
                recipientUser.TelephoneServiceProvider.CreateSmsAddress(recipientUser.MobilePhoneNumber);
#if !DEBUG

            SmtpClient client = new SmtpClient(GatsbConfigurationSection.Current.SmtpServer);
            client.Send(senderAddress.ToString(),
                recipientAddress.ToString(),
                string.Empty,
                body);

#else

            Console.WriteLine("Sending message to {0} from {1}", recipientAddress, senderAddress);
            Console.WriteLine();
            Console.WriteLine(body);

#endif
        }

        public void SendMail(MailMessage message)
        {
            if ( message == null )
                throw new ArgumentNullException( "message" );
        
            MailAddress fromAddr = new MailAddress(GatsbConfigurationSection.Current.NotificationFromAddress, "Gatsb Notifications");
            message.From = fromAddr;
#if !DEBUG
            SmtpClient client = new SmtpClient(GatsbConfigurationSection.Current.SmtpServer);
            client.Send(message);
#endif
        }

        /// <summary>
        /// Ensures the session is authenticated, and that the user has a <see cref="GatsbApplication.CurrentUser"/>.
        /// If the user does not, redirects to the login page.
        /// </summary>
        public static void EnsureAuthenticatedSession()
        {
            if (!Current.UserPresent)
            {
                FormsAuthentication.SignOut();
                FormsAuthentication.RedirectToLoginPage();
                HttpContext.Current.Response.End();
                return;
            }
        }

        /// <summary>
        /// Gets or sets if this is a new account that has not yet seen the help file.
        /// </summary>
        public bool NewAccountHasNotSeenHelp
        {
            get
            {
                if (HttpContext.Current.Session["NewAccount_NoHelp"] == null)
                    return false;

                return (bool)HttpContext.Current.Session["NewAccount_NoHelp"];
            }
            set
            {
                HttpContext.Current.Session["NewAccount_NoHelp"] = value;
            }
        }

        /// <summary>
        /// Sets the name of a Gatsb web page.
        /// </summary>
        /// <param name="page">The page to set the name of.</param>
        /// <param name="name">The main title of the page.</param>
        public static void SetPageName(System.Web.UI.Page page, string name)
        {
            if (page == null)
                throw new ArgumentNullException("page");

            if (name == null)
                throw new ArgumentNullException("name");

            page.Title = string.Format("{0} - gatsb", name);
        }
    }
}
